﻿
#region Copyright & License
// --------------------------
// Copyright 2008 Daniel Gray
// --------------------------
// This file is part of Kale.
//
// Kale is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Kale is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with Kale.  If not, see <http://www.gnu.org/licenses/>.
#endregion
#region File details
// --------------------------------------------------------
// Filename          : ImageCache.cs
// Author            : Daniel Gray
// Created           : 18/04/2008
// --------------------------------------------------------
#endregion
            
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Kale.Core.Configuration;
using System.Drawing.Imaging;
using System.Drawing;

namespace Kale.Core
{
   public class ImageCache
   {
      private static DirectoryInfo _cacheDirectory;
      private static long _maximumCacheSize;
      private static int _shrinkagePercent;
      private static long _cacheSize;
      private static int _numberOfFilesInCache;

      private const int _bytesToMegabytes = 1048576;

      private static object _cacheLock = new object();

      /// <summary>
      /// Initializes the <see cref="ImageCache"/> class.
      /// </summary>
      static ImageCache()
      {
         Configure(KaleConfigurationSection.Default.ImageCaching.CachePath, 
                   KaleConfigurationSection.Default.ImageCaching.MaximumCacheSize, 
                   KaleConfigurationSection.Default.ImageCaching.ShrinkagePercent);
      }

      /// <summary>
      /// Configures the specified cache directory.
      /// </summary>
      /// <param name="cacheDirectory">The cache directory.</param>
      /// <param name="maximumCacheSize">Maximum size of the cache.</param>
      public static void Configure(string cacheDirectory, int maximumCacheSize, int shrinkagePercent)
      {
         _cacheDirectory = new DirectoryInfo(cacheDirectory);

         _maximumCacheSize = maximumCacheSize * _bytesToMegabytes;
         _shrinkagePercent = shrinkagePercent;

         if (_cacheDirectory.Exists)
         {

            LoadCacheState();

            lock (_cacheLock)
            {
               ShrinkCacheToSize(_maximumCacheSize);
            }

         }
      }

      private static void ShrinkCache()
      {
         float p = (float)_shrinkagePercent / 100f;
         long newSize = (long)((float)_maximumCacheSize * p);

         ShrinkCacheToSize(newSize);
      }

      /// <summary>
      /// Loads the state of the cache.
      /// </summary>
      public static void LoadCacheState()
      {
         FileInfo[] files = _cacheDirectory.GetFiles();
         _cacheSize = files.Sum(f => f.Length);
         _numberOfFilesInCache = files.Length;
      }

      /// <summary>
      /// Shrinks the size of the cache.
      /// </summary>
      /// <param name="requiredSize">Required size of the cache.</param>
      private static void ShrinkCacheToSize(long requiredSize)
      {
         FileInfo[] cacheFiles = _cacheDirectory.GetFiles().OrderBy(f => f.LastAccessTime).ToArray();
         List<FileInfo> cacheFilesToPurge = new List<FileInfo>();
         int cacheFileIndex = 0;

         long newSize = _cacheSize;

         while (newSize > requiredSize && cacheFileIndex < cacheFiles.Length)
         {
            cacheFilesToPurge.Add(cacheFiles[cacheFileIndex]);
            newSize -= cacheFiles[cacheFileIndex].Length;
            cacheFileIndex++;
         }

         // delete the files in the list now
         foreach (FileInfo cacheFileToPurge in cacheFilesToPurge)
         {
            File.Delete(cacheFileToPurge.FullName);
            _numberOfFilesInCache--;
            _cacheSize -= cacheFileToPurge.Length;
         }
      }

      public static int NumberOfFilesInCache
      {
         get { return _numberOfFilesInCache; }
      }

      public static long CacheSize
      {
         get { return _cacheSize; }
      }

      /// <summary>
      /// Determines whether has image in cache.
      /// </summary>
      /// <param name="cacheKey">The cache key.</param>
      /// <param name="format">The format.</param>
      /// <returns>
      /// 	<c>true</c> if has image in cache otherwise, <c>false</c>.
      /// </returns>
      public static bool HasImageInCache(IImageNode image, Size size, ImageFormat format)
      {
         FileInfo cachedFile = new FileInfo(GetCacheFilePath(image.NodeId.GetCacheKey(size), format));

         if (CheckAndPurgeChangedFile(image.NodeInformation, cachedFile)) return false;

         if (cachedFile.Exists)
         {
            return true;
         }
         else
         {
            return false;
         }
      }

      private static bool CheckAndPurgeChangedFile(FileSystemInfo current, FileInfo cached)
      {
         if (current.LastWriteTime > cached.LastWriteTime)
         {
            // changed - purge it
            cached.Delete();
            return true;
         }
         else
         {
            return false;
         }
      }

      /// <summary>
      /// Gets the cache file path.
      /// </summary>
      /// <param name="cacheKey">The cache key.</param>
      /// <param name="format">The format.</param>
      /// <returns></returns>
      private static string GetCacheFilePath(string cacheKey, ImageFormat format)
      {
         return Path.Combine(_cacheDirectory.FullName, cacheKey + "." + format.ToString());
      }

      /// <summary>
      /// Gets the image from cache.
      /// </summary>
      /// <param name="cacheKey">The cache key.</param>
      /// <param name="format">The format.</param>
      /// <returns></returns>
      public static byte[] GetImageFromCache(IImageNode image, Size size, ImageFormat format)
      {
         FileInfo cachedFile = new FileInfo(GetCacheFilePath(image.NodeId.GetCacheKey(size), format));

         CheckAndPurgeChangedFile(image.NodeInformation, cachedFile);

         if (cachedFile.Exists)
         {
            return File.ReadAllBytes(cachedFile.FullName);
         }
         else
         {
            return null;
         }
      }

      /// <summary>
      /// Adds the image to cache.
      /// </summary>
      /// <param name="cacheKey">The cache key.</param>
      /// <param name="format">The format.</param>
      /// <param name="image">The image.</param>
      public static void AddImageToCache(IImageNode imageNode, Size size, ImageFormat format, byte[] image)
      {
         string key = imageNode.NodeId.GetCacheKey(size);

         if (!HasImageInCache(imageNode, size, format))
         {
            lock (_cacheLock)
            {
               if ((_cacheSize + image.LongLength) > _maximumCacheSize)
               {
                  ShrinkCache();
               }

               // write to the cache
               string filePath = GetCacheFilePath(key, format);

               File.WriteAllBytes(filePath, image);

               // increment values
               _cacheSize += image.Length;
               _numberOfFilesInCache++;
            }
         }
      }
   }
}
